package pkmn.battle;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import pkmn.Pokemon;
import pkmn.attributes.BattleAttributes;
import pkmn.exception.DeathException;
import pkmn.move.Move;
import pkmn.move.IndirectAction;
import pkmn.status.BattleStatus;
import pkmn.status.BattleStatusEnum;
import pkmn.status.StatusEnum;
import pkmn.util.Util;

public class BattlePokemon {

    private Pokemon pokemon;
    private BattleAttributes battleAttributes;
    private List<BattleStatus> battleStatus;
    private boolean attackEnabled;
    private int criticalLevel;
    private double attackLevel;

    public BattlePokemon(Pokemon pokemon) {
        this.pokemon = pokemon;
        this.battleStatus = new ArrayList<BattleStatus>();
        this.battleAttributes = new BattleAttributes();
        this.attackEnabled = true;
        this.criticalLevel = 1;
        this.attackLevel = 1;
    }

    public Pokemon getPokemon() {
        return pokemon;
    }

    public void setPokemon(Pokemon pokemon) {
        this.pokemon = pokemon;
        //remove status and attributes
        this.battleStatus.clear();
        this.battleAttributes = new BattleAttributes();
        this.criticalLevel = 1;
        this.attackLevel = 1;
    }

    public boolean canAttack() {
        return this.attackEnabled;
    }

    public void canAttack(boolean attackEnabled) {
        this.attackEnabled = attackEnabled;
    }

    /**
     * Return a list of battle status
     * @return List<BattleStatus>
     */
    public List<BattleStatus> getBattleStatus() {
        return this.battleStatus;
    }

    /**
     * Set a battle status
     * @param battleStatusEnum
     */
    public void setBattleStatus(BattleStatusEnum battleStatusEnum) {
        this.battleStatus.add(new BattleStatus(battleStatusEnum));
    }

    /**
     * Set a battle status
     * @param battleStatusEnum BattleStatus
     * @param turns int Number of turns
     */
    public void setBattleStatus(BattleStatusEnum battleStatusEnum, int turns) {
        this.battleStatus.add(new BattleStatus(battleStatusEnum, turns));
    }

    public boolean haveBattleStatus(BattleStatusEnum battleStatusEnum) {
        for (BattleStatus be : this.battleStatus) {
            if (be.getStatusEnum() == battleStatusEnum) {
                return true;
            }
        }
        return false;
    }

    /**
     * Remove a battle status
     * @param battleStatusEnum BattleStatusEnum
     */
    public void removeBattleStatus(BattleStatusEnum battleStatusEnum) {
        for (BattleStatus be : new ArrayList<BattleStatus>(this.battleStatus)) {
            if (be.getStatusEnum() == battleStatusEnum) {
                this.battleStatus.remove(be);
            }
        }
    }

    public BattleAttributes getBattleAttributes() {
        return this.battleAttributes;
    }

    public double getAttackLevel() {
        return this.attackLevel;
    }

    public void setAttackLevel(double attackLevel) {
        this.attackLevel = attackLevel;
    }

    /**
     * Increase attack
     * @return boolean (false if can't increase)
     */
    public boolean incAttack() {
        return this.battleAttributes.incAttack();
    }

    /**
     * Decrease attack
     * @return
     */
    public boolean decAttack() {
        return this.battleAttributes.decAttack();
    }

    /**
     * Increase defense
     * @return boolean (false if can't increase)
     */
    public boolean incDefense() {
        return this.battleAttributes.incDefense();
    }

    /**
     * Decrease defense
     * @return boolean (false if can't decrease)
     */
    public boolean decDefense() {
        return this.battleAttributes.decDefense();
    }

    /**
     * Increase speed
     * @return boolean (false if can't increase)
     */
    public boolean incSpeed() {
        return this.battleAttributes.incSpeed();
    }

    /**
     * Decrease defense
     * @return boolean (false if can't decrease)
     */
    public boolean decSpeed() {
        return this.battleAttributes.decSpeed();
    }

    /**
     * Increase special attack
     * @return boolean (false if can't increase)
     */
    public boolean incSpAttack() {
        return this.battleAttributes.incSpAttack();
    }

    /**
     * Decrease special attack
     * @return boolean (false if can't decrease)
     */
    public boolean decSpAttack() {
        return this.battleAttributes.decSpAttack();
    }

    /**
     * Increase special defense
     * @return boolean (false if can't increase)
     */
    public boolean incSpDefense() {
        return this.battleAttributes.incSpDefense();
    }

    /**
     * Decrease special defense
     * @return boolean (false if can't decrease)
     */
    public boolean decSpDefense() {
        return this.battleAttributes.decSpDefense();
    }

    /**
     * Decrease accuracy
     * @return boolean (false if can't decrease)
     */
    public boolean incAccuracy() {
        return this.battleAttributes.incAccuracy();
    }

    /**
     * Decrease accuracy
     * @return boolean (false if can't decrease)
     */
    public boolean decAccuracy() {
        return this.battleAttributes.decAccuracy();
    }

    /**
     * Decrease evasion
     * @return boolean (false if can't decrease)
     */
    public boolean incEvasion() {
        return this.battleAttributes.incEvasion();
    }

    /**
     * Decrease evasion
     * @return boolean (false if can't decrease)
     */
    public boolean decEvasion() {
        return this.battleAttributes.decEvasion();
    }

    public double getSpeed() {
        //if paralized, lower speed
        if (this.pokemon.getStatus().equals(StatusEnum.paralyzis)) {
            return 0.0;
        }
        return this.pokemon.getAttributes().speed;
    }

    public boolean fainted() {
        return this.pokemon.getStatus().equals(StatusEnum.fainted);
    }

    //Visual functions
    public String getVisualLife() {
        int barLength = 30;
        String ret = this.toString() + this.pokemon.getGenre() + " lv " + this.pokemon.getLevel() + "  " + this.pokemon.getStatus() + "\n";
        int lifeDecimal = (int) Math.floor((this.pokemon.getLife() * 1.0 / this.pokemon.getMaxLife()) * barLength);
        for (int i = 0; i < barLength; i++) {
            ret += (i < lifeDecimal ? "▓" : "░");
        }
        return ret + " " + this.pokemon.getLife() + "/" + this.pokemon.getMaxLife();
    }

    public String getMovesMenu() {
        String ret = "Choose a attack:";
        for (int i = 0; i < 4; i++) {
            if (this.pokemon.getMove()[i] == null) {
                break;
            }

            if (!ret.equals("")) {
                ret += "\n";
            }
            ret += (i + 1) + "- " + this.pokemon.getMove()[i]
                    + " (" + this.pokemon.getMove()[i].getPP()
                    + "/" + this.pokemon.getMove()[i].getMaxPP() + ")";
        }
        return ret;
    }

    /**
     * Increment critical level.
     * @return
     */
    public boolean incCriticalLevel() {
        if (this.criticalLevel < 6) {
            this.criticalLevel++;
            return true;
        }
        return false;
    }

    /**
     * Add critical level.
     * @param criticalLevel int
     */
    public void addCriticalLevel(int criticalLevel) {
        this.criticalLevel = Util.min(this.criticalLevel + criticalLevel, 6);
    }

    /**
     * Get the critical level.
     * @return int
     */
    public int getCriticalLevel() {
        return this.criticalLevel;
    }

    @Override
    public String toString() {
        return Util.nvl(this.pokemon.getNickname(), this.pokemon.getName());
    }

    /**
     * Attack other pokémon
     * @param index int
     * @param pokemon Pokemon
     * @return boolean (false if can't use the specified attack)
     * @throws DeathException
     */
    public boolean attack(int index, BattlePokemon battlePokemon) throws DeathException {
        Move move = this.getPokemon().getMove()[index];
        //check for PP
        if (move.getPP() == 0) {
            Util.printAndWait("There's no PP left for this move!");
            return false;
        }

        Util.printAndWait(this + " uses " + move + ".");
        move.decPP();

        //check if pokémon is protected
        if (battlePokemon.haveBattleStatus(BattleStatusEnum.protect)){
            Util.printAndWait(battlePokemon + " was protected!");
            return true;
        }

        //If is a move in itself, ever hit.
        if (!move.isSelf() && !Util.probability(move.getAccuracy()
                * (this.battleAttributes.getAccuracy()
                / battlePokemon.getBattleAttributes().getEvasion()))) {
            Util.printAndWait("But, it failed!");
            return true;
        }
        //Direct attacks
        if (move.isDirect()) {
            //calculate type effect
            double totalEffect = 1.0;
            for (int i = 0; i < battlePokemon.getPokemon().getTypes().length; i++) {
                totalEffect *= move.getType().effective(battlePokemon.getPokemon().getType(i));
            }

            if (totalEffect == 0) {
                Util.printAndWait("No effect!");
                return true;
            }
            //Multiple hits
            int multiplicy = move.getMultiplicy();
            if (multiplicy == 0) {
                multiplicy = new Random().nextInt(4) + 2;
            }
            for (int j = 0; j < multiplicy; j++) {
                if (j > 0) {
                    Util.printAndWait("The attack hits...");
                }
                //calculate critical hits
                boolean critical = Util.probability(Util.criticalRate(this.criticalLevel + move.getCriticalLevel()));
                if (critical) {
                    Util.printAndWait("Critical Hit!");
                }

                if (totalEffect > 1.0) {
                    Util.printAndWait("It's super effective!");
                } else if (totalEffect < 1.0) {
                    Util.printAndWait("It's not very effective!");
                }
                //Calculate damage
                double STAB = 1.0;
                for (int i = 0; i < this.pokemon.getTypes().length; i++) {
                    if (move.getType().equals(this.pokemon.getTypes()[i])) {
                        STAB = 1.5;
                        break;
                    }
                }
                double atkLevel = critical ? 1 : this.attackLevel;
                //http://www.serebii.net/games/damage.shtml
                double attackCalculate = move.isSpecial() ? this.pokemon.getAttributes().spAttack : this.pokemon.getAttributes().attack * atkLevel;
                double defenseCalculate = move.isSpecial() ? battlePokemon.getPokemon().getAttributes().spDefense : battlePokemon.getPokemon().getAttributes().defense;

                attackCalculate *= move.isSpecial() ? this.battleAttributes.getSpAttack() : this.battleAttributes.getAttack();
                //if critical, ignore changes in enemy defense
                if (!critical) {
                    defenseCalculate *= move.isSpecial() ? battlePokemon.battleAttributes.getSpDefense() : battlePokemon.battleAttributes.getDefense();
                }

                int damage = (int) (((((2.0 * this.getPokemon().getLevel() / 5.0 + 2.0)
                        * attackCalculate
                        * move.getPower()
                        / defenseCalculate)
                        / 50.0) + 2.0)
                        * STAB * totalEffect * Util.factor());
                if (critical) {
                    damage *= 2;
                }
                //
                Util.printAndWait("Damage: " + damage);
                //Indirect action of move
                battlePokemon.getPokemon().receiveDamage(damage);
                IndirectAction indirectAction = move.getIndirectAction();
                if (indirectAction != null) {
                    if (Util.probability(move.getIndirectAccuracy())) {
                        indirectAction.execute(move.isSelf() ? this : battlePokemon);
                    }
                }
            }
            //Indirect attacks
        } else {
            if (Util.probability(move.getIndirectAccuracy())) {
                move.getIndirectAction().execute(move.isSelf() ? this : battlePokemon);
            } else {
                Util.printAndWait("But, it failed!");
            }
        }
        return true;
    }
}
